<?php

namespace W3;

/**
 * Request 表示一个HTTP请求
 * 默认的Request属性就包括url,base,method,user_agent等。
 *
 * @author edikud
 * @date 2022/10/22
 * @copyright Copyright (c) 2022 W3 (http://www.mcooo.com)
 * @license GNU General Public License 2.0
 */

class Request
{
    /**
     * 当前属性数据
     * @var array
     */
    protected $data = [];

    /**
     * 内部参数
     * @var array
     */
    protected $params = [];
	
    /**
     * 当前过滤器
     *
     * @access private
     * @var array
     */
    private $_filter = [];

    /**
     * 支持的过滤器列表
     *
     * @access private
     * @var string
     */
    private static $_supportFilters = [
        'int'       =>  'intval',
        'integer'   =>  'intval',
        'search'    =>  [Util::class, 'safeUrl'],
        'xss'       =>  [Util::class, 'removeXss'],
        'url'       =>  [Util::class, 'safeUrl']
    ];

    /**
     * 单例句柄
     *
     * @access protected
     * @var Request
     */
    protected static $_instance;
	
    /**
     * 单例实例
     *
     * @return Request
     */
    public static function instance(): Request
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }

    /**
     * 创建新实例
     *
     * @access public
     * @return Request
     */
    public static function make(): Request
    {
        return new static();
    }
	
    /**
     * 设置server数据
     * 
     * @param string $name 配置名称
     * @param mixed $value 配置值
     */
    public function data($name, $value = NULL): Request
    {
        if (is_array($name)) {
			
			$this->data = array_merge($this->data, $name);
        } else {
			
			$this->data[$name] = $value;
		}

		return $this;
    }
	
    /**
     * 设置params数据
     * 
     * @param string $name 配置名称
     * @param mixed $value 配置值
     */
    public function setParams($params): Request
    {
        //处理字符串
        if (!is_array($params)) {
            parse_str($params, $out);
            $params = $out;
        }
		
        $this->params = array_merge($this->params, $params);
		return $this;
    }
	
    /**
     * 获取实际传递参数
     *
     * @access public
     * @param string $key 指定参数
     * @param mixed $default 默认参数 (default: NULL)
     * @return mixed
     */
    public function get(string $name, $default = null)
    {
		$value = null;
		
        switch (true) {
            case isset($this->params[$name]):
                $value = $this->params[$name];
                break;
            case isset($_GET[$name]):
                $value = $_GET[$name];
                break;
            case isset($_POST[$name]):
                $value = $_POST[$name];
                break;
        }

        if (isset($value)) {
			if (is_array($default)){
				!is_array($value) && $value = $default;
			} else {
				!(is_string($value) && strlen($value) > 0) && $value = $default;
			}
        } else {
            $value = $default;
        }

		return $this->_applyFilter($value);
    }
	
    /**
     * 获取一个数组
     *
     * @param $key
     * @param $only 是否指定是[]类型的参数  默认：否
     * @return array
     */
    public function getArray($key, bool $only = false): array
    {
		# $key[] = **
        $result = $this->get($key, []);

        if (!empty($result) || $only) {
            return $result;
        }
		
		# $key = **
		$result = $this->get($key);

        return !empty($result) ? [$result] : [];
    }

    /**
     * 从参数列表指定的值中获取http传递参数
     *
     * @access public
     * @param mixed $params 指定的参数
     * @return array
     */
    public function from($params): array
    {
        $result = [];
        $args = is_array($params) ? $params : func_get_args();

        foreach ($args as $arg) {
            $result[$arg] = $this->get($arg);
        }

        return $result;
    }
	
    /**
     * 设置过滤器
     *
     * @access public
     * @return Request
     */
    public function setFilters($name, $callback = NULL): Request
    {
        if ( ! is_array($name)) {
            $name = [$name => $callback];
        }

        foreach ($name as $key => $val)
		{
            static::$_supportFilters[$key] = $val;
        }
        return $this;
    }
	
    /**
     * 设置过滤器
     *
     * @param string|callable ...$filters
     * @return $this
     */
    public function filter(...$filters): Request
    {
        foreach ($filters as $filter) {
            $this->_filter[] = is_string($filter) && isset(self::$_supportFilters[$filter])
                ? self::$_supportFilters[$filter] : $filter;
        }

        return $this;
    }

    /**
     * 应用过滤器
     *
     * @access private
     * @param mixed $value
     * @return mixed
     */
    private function _applyFilter($value)
    {
        if ($this->_filter) {
            foreach ($this->_filter as $filter) {
                $value = is_array($value) ? array_map($filter, $value) :
                call_user_func($filter, $value);
            }

            $this->_filter = [];
        }

        return $value;
    }
	
    /**
     * 获取param参数
     * @access public
     * @param  string $name 数据名称
     * @param  string $default 默认值
     * @return mixed
     */
    public function param($name = null, $default = null)
    {
		// 无参数时获取所有
        if (null === $name) {
            return $this->params;
        }
		
		return $this->params[$name] ?? $default;
    }
	
    /**
     * 获取server参数
     * @access public
     * @param  string $name 数据名称
     * @param  string $default 默认值
     * @return mixed
     */
    public static function server(string $name, $default = null)
    {
		return $_SERVER[$name] ?? $default;
    }
	
    /**
     * 客户端的IP地址
     * @access public
     * @return string
     */
    public function ip(bool $long = false): string
    {
		if(!isset($this->data['ip'])){
            $this->data['ip'] = long2ip(ip2long(static::server('REMOTE_ADDR')));
		}
		
		/** 获取 longip */
		if($long){
            $longIp = ip2long($this->data['ip']);
			
	        # fix 出现负数解决方法
            $longIp < 0 and $longIp = sprintf("%u", $longIp);
			$this->data['ip'] = $longIp;
		}
		
        return $this->data['ip'];
    }
	
    /**
     * 浏览器信息
     * @access public
     * @return string
     */
    public function agent(): string
    {
		if(isset($this->data['agent'])){
            return $this->data['agent'];
		}

        return $this->data['agent'] = static::server('HTTP_USER_AGENT', '');
    }
	
    /**
     * 网站来路
     * @access public
     * @return string
     */
    public function referer($default = NULL): ?string
    {
		if(!isset($this->data['referer'])){
			$this->data['referer'] = static::server('HTTP_REFERER', '');
		}

        return !empty($this->data['referer']) ? $this->data['referer'] : $default;
    }
	
    /**
     * 当前请求的host
     * @access public
     * @param bool $strict  true 仅仅获取HOST
     * @return string
     */
    public function host(bool $strict = false): string
    {
		if(!isset($this->data['port'])){
            $this->data['host'] = strval(static::server('HTTP_X_FORWARDED_HOST') ?: static::server('HTTP_HOST'));
		}

		true === $strict && strpos($this->data['host'], ':') && $this->data['host'] = strstr($this->data['host'], ':', true);

        return $this->data['host'];
    }

    /**
     * 当前请求URL地址中的port参数
     * @access public
     * @return int
     */
    public function port(): int
    {
		if(isset($this->data['port'])){
            return $this->data['port'];
		}
		$port = (int) (static::server('HTTP_X_FORWARDED_PORT') ?: static::server('SERVER_PORT', ''));
		
        return $this->data['port'] = $port;
    }

    /**
     * 当前请求 SERVER_PROTOCOL
     * @access public
     * @return string
     */
    public function protocol($default = ''): string
    {
		if(!isset($this->data['protocol'])){
            $this->data['protocol'] = static::server('SERVER_PROTOCOL', '');
		}

        return $this->data['protocol'] ?  $this->data['protocol'] : $default;
    }
	
    /**
     * 获取当前包含协议的域名
     * @access public
     * @param  bool $port 是否需要去除端口号
     * @return string
     */
    public function domain(bool $port = false): string
    {
		if(isset($this->data['domain'])){
            return $this->data['domain'];
		}

        return $this->data['domain'] = $this->scheme() . '://' . $this->host($port);
    }
	
    /**
     * 当前URL地址中的scheme参数
     * @access public
     * @return string
     */
    public function scheme(): string
    {
		if(isset($this->data['scheme'])){
            return $this->data['scheme'];
		}

        return $this->data['scheme'] = $this->ssl() ? 'https' : 'http';
    }
	
    /**
     * 判断是否为https
     *
     * @access public
     * @return boolean
     */
    public function ssl(): bool
    {
		if(isset($this->data['ssl'])){
            return $this->data['ssl'];
		}
		
		$ssl = (!empty(static::server('HTTPS')) && 'on' == strtolower(static::server('HTTPS'))) 
            || (443 == static::server('SERVER_PORT'));

        return $this->data['ssl'] = $ssl;
    }
	
    /**
     * 获取当前URL 不含QUERY_STRING
     * @access public
     * @param  bool $complete 是否包含完整域名
     * @return string
     */
    public function baseUrl(bool $complete = false): string
    {
		if(!isset($this->data['baseUrl'])){
            $str           = $this->url();
            $this->data['baseUrl'] = strpos($str, '?') ? strstr($str, '?', true) : $str;
		}
		
        return $complete ? $this->domain() . $this->data['baseUrl'] : $this->data['baseUrl'];
    }
	
    /**
     * 获取当前完整URL 包括QUERY_STRING
     * @access public
     * @param  bool $complete 是否包含完整域名
     * @return string
     */
    public function url(bool $complete = false): string
    {
		if(!isset($this->data['url'])){
			
            if ($this->server('HTTP_X_REWRITE_URL')) {
                $url = $this->server('HTTP_X_REWRITE_URL');
            } elseif ($this->server('REQUEST_URI')) {
                $url = $this->server('REQUEST_URI');
            } elseif ($this->server('ORIG_PATH_INFO')) {
                $url = $this->server('ORIG_PATH_INFO') . (!empty($this->server('QUERY_STRING')) ? '?' . $this->server('QUERY_STRING') : '');
            } elseif (isset($_SERVER['argv'][1])) {
                $url = $_SERVER['argv'][1];
            } else {
                $url = '';
			}
			
			$this->data['url'] = $url;
		}
		
		return $complete ? $this->domain() . $this->data['url'] : $this->data['url'];
    }
	
    /**
     * 获取当前执行的文件 SCRIPT_NAME
     * @access public
     * @param  bool $complete 是否包含完整域名
     * @return string
     */
    public function baseFile(bool $complete = false): string
    {
        if (!isset($this->data['baseFile'])) {
            $url = '';
            if (!$this->isCli()) {
                $script_name = basename($this->server('SCRIPT_FILENAME'));
                if (basename($this->server('SCRIPT_NAME')) === $script_name) {
                    $url = $this->server('SCRIPT_NAME');
                } elseif (basename($this->server('PHP_SELF')) === $script_name) {
                    $url = $this->server('PHP_SELF');
                } elseif (basename($this->server('ORIG_SCRIPT_NAME')) === $script_name) {
                    $url = $this->server('ORIG_SCRIPT_NAME');
                } elseif (($pos = strpos($this->server('PHP_SELF'), '/' . $script_name)) !== false) {
                    $url = substr($this->server('SCRIPT_NAME'), 0, $pos) . '/' . $script_name;
                } elseif ($this->server('DOCUMENT_ROOT') && strpos($this->server('SCRIPT_FILENAME'), $this->server('DOCUMENT_ROOT')) === 0) {
                    $url = str_replace('\\', '/', str_replace($this->server('DOCUMENT_ROOT'), '', $this->server('SCRIPT_FILENAME')));
                }
            }
            $this->data['baseFile'] = $url;
        }

        return $complete ? $this->domain() . $this->data['baseFile'] : $this->data['baseFile'];
    }

    /**
     * 获取应用程序的根地址
     * @access public
     * @param  bool $complete 是否包含完整域名
     * @return string
     */
    public function root(bool $complete = false)
    {
		if(!isset($this->data['root'])){
			
            $file = $this->baseFile();
            if ($file && 0 !== strpos($this->url(), $file)) {
                $file = str_replace('\\', '/', dirname($file));
            }
            $this->data['root'] = rtrim($file, '/');
		}

        return $complete ? $this->domain() . $this->data['root'] : $this->data['root'];
    }
	
    /**
     * 获取URL访问根目录
     * @access public
     * @return string
     */
    public function rootUrl(bool $complete = false): string
    {
        if(!isset($this->data['rootUrl'])){
            $root = $this->root();

            $root = strpos($root, '.') ? ltrim(dirname($root), DIRECTORY_SEPARATOR) : $root;

            if ('' != $root) {
                $root = '/' . ltrim($root, '/');
            }

            $this->data['rootUrl'] = $root;

        }

		return $complete ? $this->domain() . $this->data['rootUrl'] : $this->data['rootUrl'];
    }
	
    /**
     * 获取当前请求URL的pathinfo信息（含URL后缀）
     * @access public
     * @return string
     */
    public function pathinfo()
    {
		if(isset($this->data['pathinfo'])){
            return $this->data['pathinfo'];
		}
		
        if ($this->server('PATH_INFO')) {
            $pathinfo = $this->server('PATH_INFO');
        } elseif (false !== strpos(PHP_SAPI, 'cli')) {
            $pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
        }

        // 分析PATHINFO信息
        if (!isset($pathinfo)) {
            foreach (['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'] as $type) 
			{
                if ($this->server($type)) {
                    $pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) 
					    ? substr($this->server($type), strlen($this->server('SCRIPT_NAME')))
					    : $this->server($type);
                    break;
                }
            }
        }
		
		empty($pathinfo) && $pathinfo = '/';
		
        // remove all characters except letters,
        // digits and $-_.+!*'(),{}|\\^~[]`<>#%";/?:@&=.
        //$pathinfo = empty($pathinfo)
  		//    ? '/' 
		//	: filter_var(rawurldecode($pathinfo), FILTER_SANITIZE_URL);
		
		return $this->data['pathinfo'] = '/' . trim($pathinfo, '/');
    }

    /**
     * 当前的请求类型
     * @access public
     * @param  bool $origin 是否获取原始请求类型
     * @return string
     */
    public function method()
    {
		if(isset($this->data['method'])){
            return $this->data['method'];
		}
		
        return $this->data['method'] = strtoupper(static::server('REQUEST_METHOD')) ?: 'GET';
    }

    /**
     * 判断是否为get方法
     *
     * @access public
     * @return boolean
     */
    public function isGet(): bool
    {
		if(isset($this->data['isGet'])){
            return $this->data['isGet'];
		}
		
        return $this->data['isGet'] = 'GET' == $this->method();
    }

    /**
     * 判断是否为post方法
     *
     * @access public
     * @return boolean
     */
    public function isPost(): bool
    {
		if(isset($this->data['isPost'])){
            return $this->data['isPost'];
		}
		
        return $this->data['isPost'] = 'POST' == $this->method();
    }

    /**
     * 判断是否为put方法
     *
     * @access public
     * @return boolean
     */
    public function isPut(): bool
    {
		if(isset($this->data['isPut'])){
            return $this->data['isPut'];
		}
		
        return $this->data['isPut'] = 'PUT' == $this->method();
    } 

    /**
     * 判断是否为ajax
     *
     * @access public
     * @return boolean
     */
    public function isAjax(): bool
    {
		if(isset($this->data['isAjax'])){
            return $this->data['isAjax'];
		}
		
        return $this->data['isAjax'] = 'XMLHttpRequest' == $this->server('HTTP_X_REQUESTED_WITH');
    }

    /**
     * 是否为cli
     * @access public
     * @return bool
     */
    public function isCli(): bool
    {
		if(isset($this->data['isCli'])){
            return $this->data['isCli'];
		}
		
        return $this->data['isCli'] = PHP_SAPI == 'cli';
    }

    /**
     * 是否为cgi
     * @access public
     * @return bool
     */
    public function isCgi(): bool
    {
		if(isset($this->data['isCgi'])){
            return $this->data['isCgi'];
		}
		
        return $this->data['isCgi'] = strpos(PHP_SAPI, 'cgi') === 0;
    }
	
    /**
     * 当前是否Pjax请求
     * @access public
     * @param  bool $pjax true 获取原始pjax请求
     * @return bool
     */
    public function isPjax(): bool
    {
		if(isset($this->data['isPjax'])){
            return $this->data['isPjax'];
		}
		
        return $this->data['isPjax'] = !empty($this->server('HTTP_X_PJAX'));
    }
	
    /**
     * 获取实际传递参数(magic)
     *
     * @access public
     * @param string $key 指定参数
     * @return mixed
     */
    public function __get($key)
    {
        return $this->get($key);
    }

    /**
     * 判断参数是否存在
     *
     * @access public
     * @param string $key 指定参数
     * @return boolean
     */
    public function __isset($key): bool
    {
        return null !== $this->get($key);
    }
	
    /**
     * 根据当前uri构造指定参数的uri
     *
     * @access public
     * @param mixed $parameter 指定的参数
     * @return string
     */
    public function makeUrl($parameter = NULL)
    {
        /** 初始化地址 */
        $requestUri = $this->url(true);
        $parts = parse_url($requestUri);

        /** 初始化参数 */
        if (is_string($parameter)) {
            parse_str($parameter, $args);
        } else if (is_array($parameter)) {
            $args = $parameter;
        } else {
            return $requestUri;
        }

        /** 构造query */
        if (isset($parts['query'])) {
            parse_str($parts['query'], $currentArgs);
            $args = array_merge($currentArgs, $args);
        }
        $parts['query'] = http_build_query($args);

        /** 返回地址 */
        return Util::buildUrl($parts);
    }
	
    /**
     * 判断输入是否满足要求
     *
     * @access public
     * @param mixed $query 条件
     * @return boolean
     */
    public function is($query): bool
    {
        $validated = false;

        /** 解析串 */
        if (is_string($query)) {
            parse_str($query, $params);
        } else if (is_array($query)) {
            $params = $query;
        }

        /** 验证串 */
        if ($params) {
            $validated = true;
            foreach ($params as $key => $val) 
			{
                $validated = empty($val) ? $this->__isset($key) : ($val == $this->get($key));

                if (!$validated) {
                    break;
                }
            }
        }

        return $validated;
    }
}
